/*
 * Copyright (c) 2017, Baidu Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.rebar.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.sql.Date;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.annotate.JsonProperty;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;

/**
 * A util to build java model class to js model class for the codes based on
 * closure library. Because for the code generated by closure compiler's
 * advanced mode, the class field name will be changed, so, for the model class
 * responded by java web server as json format, can not be used by js code
 * directory. This util can generate the corresponding js model.
 *
 * @author copperybean.zhang@gmail.com
 */
public class JSModelBuilder {

  private static final String JS_BASE_MODEL = "rebar.mvc.Model";

  private static final String JS_TYPE_BOOLEAN = "boolean";
  private static final String JS_TYPE_NUMBER = "number";
  private static final String JS_TYPE_STRING = "string";
  private static final String JS_TYPE_DATE = "Date";

  private static Map<String, String> baseTypeMap;
  private static Map<String, String> jsTypeInitValMap;

  @Option(name = "--jsBaseModel", required = false, usage = "The js base model, default is " + JS_BASE_MODEL)
  private String jsBaseModel;

  @Option(name = "--javaPackage", required = true, usage = "The java package names whose classes will be generated to js")
  private List<String> javaModelPackages;

  @Option(name = "--jsTargetPath", required = true, usage = "The target paths of js file for each package name "
      + "specified by javaPackage. The items number can be less than items number of javaPackage, "
      + "the default path for java packages will be the last item in this option.")
  private List<File> jsFilePaths;

  @Option(name = "--jsNS", usage = "The namespaces of generated js model class corresponding to each "
      + "name in javaPackage. If not specified, the default js namespace is same as java package name.")
  private List<String> jsNamespaces;

  @Option(name = "--classPattern", usage = "Only class name matches this pattern in the specified packages will be built")
  private Pattern javaModelClassPattern;

  @Option(name = "--jsReferClassMap", usage = "Sometimes, you may not satisfy with some methods such as toJson to the "
      + "auto generated model class such as GA. A good troice is to write a class EGA extends the class GA and "
      + "override the method toJson. But there may be another auto generated class GB who has a field ga with "
      + "type of GA, you should know the toJson method in GB will call toJson method of ga. Since you have "
      + "overriden the toJson method of GA in EGA, so you must want the toJson method of ga should be the EGA's "
      + "implementation. You can provide a configuration file, specifying to replace all reference of GA to EGA, "
      + "within each line is a map entry, with format key:value, where key is the java class' full name, value "
      + "is the js referring class' full name, then all reference in auto generated classes " + "will be replaced.")
  private File jsReferClassMapFile;

  @Option(name = "--jsReferNS", usage = "Similar to option jsReferClassMap, but modifying all referred class' namespaces of each package "
      + "in option javaPackage, and this option's priority is lower than jsReferClassMap.")
  private List<String> jsReferNamespaces;

  @Option(name = "--indentation", usage = "Indentation for new line in block, default is 2 spaces")
  private String indentation = "  ";

  private Map<String, String> jsReferClassMap = new HashMap<String, String>();
  private Set<String> builtClassNames = new HashSet<String>();
  private List<Class<?>> buildingClasses;

  static {
    baseTypeMap = new HashMap<String, String>();
    baseTypeMap.put(boolean.class.getSimpleName(), JS_TYPE_BOOLEAN);
    baseTypeMap.put(Boolean.class.getSimpleName(), JS_TYPE_BOOLEAN);
    baseTypeMap.put(byte.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(char.class.getSimpleName(), JS_TYPE_STRING);
    baseTypeMap.put(short.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(int.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(Integer.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(long.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(Long.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(float.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(Float.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(double.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(Double.class.getSimpleName(), JS_TYPE_NUMBER);
    baseTypeMap.put(String.class.getSimpleName(), JS_TYPE_STRING);
    baseTypeMap.put(Date.class.getSimpleName(), JS_TYPE_DATE);

    jsTypeInitValMap = new HashMap<String, String>();
    jsTypeInitValMap.put(JS_TYPE_BOOLEAN, "false");
    jsTypeInitValMap.put(JS_TYPE_NUMBER, "0");
    jsTypeInitValMap.put(JS_TYPE_STRING, "''");
    jsTypeInitValMap.put(JS_TYPE_DATE, "new Date()");
  }

  static enum JsTypeCategory {
    UNKNOW, BASE, COMPOUND, CUSTOM;
  }

  static class JSType {
    public String fullName = "*";
    public String initVal = "null";
    public JsTypeCategory category = JsTypeCategory.UNKNOW;

    public JSType() {
    }

    public JSType(String fullName, String initVal, JsTypeCategory category) {
      this.fullName = fullName;
      this.initVal = initVal;
      this.category = category;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (null == obj || getClass() != obj.getClass()) {
        return false;
      }
      JSType jsType = (JSType) obj;
      return StringUtils.equals(fullName, jsType.fullName) && StringUtils.equals(initVal, jsType.initVal)
          && category == jsType.category;
    }
  }

  /**
   * To start the build
   *
   * @return return true if succeed
   */
  public boolean build() throws Exception {
    if (jsReferClassMapFile != null) {
      BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(jsReferClassMapFile), "UTF-8"));
      try {
        initJsReferMap(br);
      } finally {
        br.close();
      }
    }
    builtClassNames.clear();
    buildingClasses = getMatchedClasses();
    for (int i = 0; i < buildingClasses.size(); ++i) {
      PrintWriter output = null;
      File jsPath = getJSFilePath(buildingClasses.get(i));
      initDirectory(jsPath.getParentFile());
      output = new PrintWriter(jsPath, "UTF-8");
      buildClass(buildingClasses.get(i), output);
    }
    return true;
  }

  private void outputRequires(PrintWriter output, Class<?> c) {
    TreeSet<String> requires = new TreeSet<String>();
    requires.add(getJSSuperType(c));
    for (Class<?> declaredClass : c.getDeclaredClasses()) {
      requires.add(getJSSuperType(declaredClass));
    }
    for (String jsType : requires) {
      output.format("goog.require('%s');\n", jsType);
    }
    output.println("\n");
  }

  /**
   * For the static fields defined in java class, output them as a static enum
   * field in js model class. Split each java static field name by underscore at
   * most two pieces, the enum field name in js class is the first piece of the
   * split result, the enum key is the second piece of the split result.
   *
   * @param output
   *          the writer to write result
   * @param c
   *          the java model class
   */
  private void outputStaticFields(PrintWriter output, Class<?> c) throws Exception {
    HashMap<String, String> enumTypeMap = new HashMap<String, String>();
    HashMap<String, ArrayList<String>> enumNamesMap = new HashMap<String, ArrayList<String>>();
    HashMap<String, ArrayList<String>> enumValuesMap = new HashMap<String, ArrayList<String>>();
    for (Field f : c.getDeclaredFields()) {
      if (!Modifier.isStatic(f.getModifiers())) {
        continue;
      }
      String fieldTypeName = f.getType().getSimpleName();
      String itemName = f.getName();
      int underlinePos = itemName.indexOf("_");
      if (!baseTypeMap.containsKey(fieldTypeName) || underlinePos <= 0 || underlinePos + 1 == itemName.length()) {
        // there should be at least one underscore in the middle of the field's
        // name
        continue;
      }
      String jsTypeName = baseTypeMap.get(fieldTypeName);
      String enumName = itemName.substring(0, underlinePos);
      enumName = enumName.substring(0, 1).toUpperCase() + enumName.substring(1).toLowerCase();
      String enumKeyName = itemName.substring(underlinePos + 1);
      if (!enumTypeMap.containsKey(enumName)) {
        enumTypeMap.put(enumName, jsTypeName);
        enumNamesMap.put(enumName, new ArrayList<String>());
        enumValuesMap.put(enumName, new ArrayList<String>());
      }
      enumNamesMap.get(enumName).add(enumKeyName);
      enumValuesMap.get(enumName).add(f.get(null).toString());
    }

    String jsTypeName = getJSType(c, false, null, null).fullName;
    for (Map.Entry<String, ArrayList<String>> entry : enumNamesMap.entrySet()) {
      boolean isJsNumType = JS_TYPE_NUMBER.equals(enumTypeMap.get(entry.getKey()));
      output.println("/**");
      if (isJsNumType) {
        output.println(" * @enum");
      } else {
        output.format(" * @enum {%s}\n", enumTypeMap.get(entry.getKey()));
      }
      output.println(" */");
      output.format("%s.%s = {\n", jsTypeName, entry.getKey());
      int count = 0;
      for (String itemName : entry.getValue()) {
        String valStr = enumValuesMap.get(entry.getKey()).get(count);
        if (!isJsNumType) {
          valStr = StringUtils.join(new String[] { "'", valStr, "'" });
        }
        output.format("%s%s: %s", indentation, itemName, valStr);
        if (++count == entry.getValue().size()) {
          output.println();
        } else {
          output.println(',');
        }
      }
      output.println("};\n");
    }
  }

  private void outputFields(PrintWriter output, Class<?> c) {
    for (Field f : c.getDeclaredFields()) {
      if (Modifier.isStatic(f.getModifiers())) {
        continue;
      }

      JSType jsType = getJSType(f.getType(), true, f.getGenericType(), c);
      output.format("\n%s/**\n%s * @type {%s}\n%s */\n", indentation, indentation, jsType.fullName, indentation);
      output.format("%sthis.%s = %s;\n", indentation, f.getName(), jsType.initVal);
    }
  }

  private void outputToJsonMethod(PrintWriter output, Class<?> c, String jsTypeName) {
    output.println("/**\n * @override\n */");
    output.format("%s.prototype.toJson = function() {\n", jsTypeName);
    output.format("%svar ret = %s.superClass_.toJson.call(this);\n", indentation, jsTypeName);
    for (Field f : c.getDeclaredFields()) {
      if (Modifier.isStatic(f.getModifiers())) {
        continue;
      }

      String fieldJsonName = getFieldJsonName(f);
      String fieldName = f.getName();
      JSType fieldJSType = getJSType(f.getType(), true, f.getGenericType(), c);
      output.format("%sret['%s'] = ", indentation, fieldJsonName);
      if (JsTypeCategory.BASE == fieldJSType.category) {
        if (JS_TYPE_DATE == fieldJSType.fullName) {
          output.format("this.%s.getTime()", fieldName);
        } else {
          output.format("this.%s", fieldName);
        }
      } else if (JsTypeCategory.COMPOUND == fieldJSType.category) {
        Type listElemType = getListElemType(f.getType(), f.getGenericType());
        if (listElemType instanceof Class) {
          JSType listElemJSType = getJSType((Class<?>) listElemType, true, null, null);
          if (JsTypeCategory.CUSTOM == listElemJSType.category) {
            output.format("this.listToJson(this.%s)", fieldName);
          } else {
            output.format("this.%s", fieldName);
          }
        } else {
          output.format("this.%s", fieldName);
        }
      } else if (JsTypeCategory.CUSTOM == fieldJSType.category) {
        if ("null".equals(fieldJSType.initVal)) {
          output.format("this.%s ? this.%s.toJson() : null", fieldName, fieldName);
        } else {
          output.format("this.%s.toJson()", fieldName);
        }
      } else {
        output.format("this.%s", fieldName);
      }
      output.println(";");
    }
    output.format("%sreturn ret;\n", indentation);
    output.println("};");
    output.println();
  }

  private void outputInitWithJsonMethod(PrintWriter output, Class<?> c, String jsTypeName) throws Exception {
    output.println("/**\n * @override\n */");
    output.format("%s.prototype.initWithJson = function(obj) {\n", jsTypeName);
    output.format("%sif (!%s.superClass_.initWithJson.call(this, obj)) {\n", indentation, jsTypeName);
    output.format("%s%sreturn false;\n", indentation, indentation);
    output.format("%s}\n", indentation);
    for (Field f : c.getDeclaredFields()) {
      if (Modifier.isStatic(f.getModifiers())) {
        continue;
      }

      String fieldJsonName = getFieldJsonName(f);
      String fieldName = f.getName();
      String javaType = f.getType().getSimpleName();
      JSType fieldJsType = getJSType(f.getType(), true, f.getGenericType(), c);
      if (JsTypeCategory.BASE == fieldJsType.category) {
        String jsType = baseTypeMap.get(javaType);
        if (jsType == JS_TYPE_BOOLEAN) {
          output.format("%stry {\n", indentation);
          output.format("%s%sthis.%s = !!goog.json.parse(obj['%s']);\n", indentation, indentation, fieldName,
              fieldJsonName);
          output.format("%s} catch (e) {\n", indentation);
          output.format("%s%s// do nothing\n", indentation, indentation);
          output.format("%s}\n", indentation);
        } else if (jsType == JS_TYPE_NUMBER) {
          output.format("%sif (goog.isNumber(obj['%s']) || !isNaN(+obj['%s'])) {\n", indentation, fieldJsonName,
              fieldJsonName);
          output.format("%s%sthis.%s = +obj['%s'];\n", indentation, indentation, fieldName, fieldJsonName);
          output.format("%s}\n", indentation);
        } else if (jsType == JS_TYPE_STRING) {
          output.format("%sthis.%s = obj['%s'] || this.%s;\n", indentation, fieldName, fieldJsonName, fieldName);
        } else if (jsType == JS_TYPE_DATE) {
          output.format("%sthis.%s = new Date(obj['%s']);\n", indentation, fieldName, fieldJsonName);
        } else {
          throw new Exception("Unsupported js base type " + jsType);
        }
      } else if (JsTypeCategory.COMPOUND == fieldJsType.category) {
        Type listElemType = getListElemType(f.getType(), f.getGenericType());
        if (listElemType instanceof Class) {
          JSType listElemJSType = getJSType((Class<?>) listElemType, true, null, null);
          if (JsTypeCategory.CUSTOM == listElemJSType.category) {
            output.format("%sthis.%s = this.initList(obj['%s'], %s);\n", indentation, fieldName, fieldJsonName,
                listElemJSType.fullName);
          } else {
            output.format("%sthis.%s = obj['%s'] || this.%s;\n", indentation, fieldName, fieldJsonName, fieldName);
          }
        } else {
          output.format("%sthis.%s = obj['%s'] || this.%s;\n", indentation, fieldName, fieldJsonName, fieldName);
        }
      } else if (JsTypeCategory.CUSTOM == fieldJsType.category) {
        if ("null".equals(fieldJsType.initVal)) {
          output.format("%sthis.%s || (this.%s = new %s());\n", indentation, fieldName, fieldName,
              fieldJsType.fullName);
        }
        output.format("%sthis.%s.initWithJson(obj['%s']);\n", indentation, fieldName, fieldJsonName);
      } else {
        output.format("%sthis.%s = obj['%s'];\n", indentation, fieldName, fieldJsonName);
      }
    }
    output.format("%sreturn true;\n", indentation);
    output.println("};\n");
  }

  private void outputModelClass(PrintWriter output, Class<?> c) throws Exception {
    builtClassNames.add(c.getName());
    String jsTypeName = getJSType(c, false, null, null).fullName;
    String jsSuperTypeName = getJSSuperType(c);
    output.println("/**");
    output.println(" * @constructor");
    output.format(" * @extends {%s}\n", jsSuperTypeName);
    output.println(" */");
    output.format("%s = function() {\n", jsTypeName);
    output.format("%s%s.call(this);\n", indentation, jsSuperTypeName);

    outputFields(output, c);

    output.format("};\ngoog.inherits(%s, %s);\n\n", jsTypeName, jsSuperTypeName);

    outputToJsonMethod(output, c, jsTypeName);
    outputInitWithJsonMethod(output, c, jsTypeName);
  }

  private void buildClass(Class<?> c, PrintWriter output) throws Exception {
    output.println("/**");
    output.println(" * @fileoverview This file is generated automatically");
    output.println(" */");
    output.format("goog.provide('%s');\n\n", getJSClassName(false, c));

    // if the types of this class' fields also be required,
    // then circulation requirement may be happened
    outputRequires(output, c);
    outputModelClass(output, c);
    outputStaticFields(output, c);

    for (Class<?> declaredClass : c.getDeclaredClasses()) {
      if (shouldBuildClass(declaredClass.getName())) {
        outputModelClass(output, declaredClass);
      }
    }

    output.close();
    System.out.format("js model file: %s generated\n", c.getSimpleName());

    if (shouldBuildClass(c.getSuperclass().getName())) {
      registerClass(c.getSuperclass());
    }
    for (Field f : c.getDeclaredFields()) {
      // all referring class should be built
      if (shouldBuildClass(f.getType().getName())) {
        registerClass(f.getType());
      }
      // the referring class as parameter of generic type should be built also
      Type[] elemTypes = getGenericParams(f.getType());
      for (Type t : elemTypes) {
        if (shouldBuildClass(t.getClass().getName())) {
          registerClass(t.getClass());
        }
      }
    }
  }

  /**
   * 获取js类的名字，包含namespace，不考虑复合结构
   *
   * @param c
   *          java的model类
   * @param isReferType
   *          参见JS_WRAPPER_NAME_SPACE的注释
   * @param fieldGenericType
   *          泛型类型，参考getListElemType
   * @param embeddingClass
   *          嵌入的class，如果c是某个类的字段，那么这里传那个类
   * @return JSType
   */
  private JSType getJSType(Class<?> c, boolean isReferType, Type fieldGenericType, Class<?> embeddingClass) {
    JSType ret = new JSType();
    Type elemType = getListElemType(c, fieldGenericType);
    if (null != elemType) {
      if (elemType instanceof Class) {
        JSType paramJSType = getJSType(((Class<?>) elemType), true, null, null);
        ret.fullName = StringUtils.join(new String[] { "Array.<", paramJSType.fullName, ">" });
      } else {
        ret.fullName = "Array";
      }
      ret.initVal = "[]";
      ret.category = JsTypeCategory.COMPOUND;
    } else if (Map.class.isAssignableFrom(c)) {
      ret.fullName = "Object";
      ret.initVal = "{}";
      ret.category = JsTypeCategory.COMPOUND;
    } else {
      if (baseTypeMap.containsKey(c.getSimpleName())) {
        ret.fullName = baseTypeMap.get(c.getSimpleName());
        ret.initVal = jsTypeInitValMap.get(ret.fullName);
        ret.category = JsTypeCategory.BASE;
      } else if (shouldBuildClass(c.getName())) {
        ret.fullName = getJSClassName(isReferType, c);
        if (null != embeddingClass && embeddingClass.isAssignableFrom(c)) {
          // If some class such as A has a field who is A its self or A's
          // subclass,
          // and we new this field's instance in A's constructor, then there
          // will be
          // infinite recursion
          ret.initVal = "null";
        } else {
          ret.initVal = "new " + ret.fullName + "()";
        }
        ret.category = JsTypeCategory.CUSTOM;
      }
    }
    return ret;
  }

  private String getJSSuperType(Class<?> c) {
    String ret = StringUtils.isEmpty(jsBaseModel) ? JS_BASE_MODEL : jsBaseModel;
    if (shouldBuildClass(c.getSuperclass().getName())) {
      ret = getJSType(c.getSuperclass(), true, null, null).fullName;
    }
    return ret;
  }

  private Type getListElemType(Class<?> c, Type fieldGenericType) {
    if (List.class.isAssignableFrom(c)) {
      Type[] paramTypes = getGenericParams(fieldGenericType);
      if (paramTypes.length == 1) {
        return paramTypes[0];
      }
    }
    return null;
  }

  private Type[] getGenericParams(Type t) {
    if (t instanceof ParameterizedType) {
      return ((ParameterizedType) t).getActualTypeArguments();
    }
    return new Type[0];
  }

  private ArrayList<Class<?>> getMatchedClasses() {
    ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
    for (String packageName : javaModelPackages) {
      Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
      Set<Class<?>> allClasses = reflections.getSubTypesOf(Object.class);
      for (Class<?> cls : allClasses) {
        if (null == javaModelClassPattern || javaModelClassPattern.matcher(cls.getName()).matches()) {
          classes.add(cls);
        }
      }
    }
    return classes;
  }

  private String getFieldJsonName(Field field) {
    JsonProperty property = field.getAnnotation(JsonProperty.class);
    if (null != property) {
      return property.value();
    }
    com.fasterxml.jackson.annotation.JsonProperty fxmlProp = field
        .getAnnotation(com.fasterxml.jackson.annotation.JsonProperty.class);
    if (null != fxmlProp) {
      return fxmlProp.value();
    }
    return field.getName();
  }

  private File getJSFilePath(Class<?> cls) throws Exception {
    String className = cls.getName();
    File path = jsFilePaths.get(jsFilePaths.size() - 1);
    int idx = getJavaPackageIndex(className);
    if (idx >= 0 && jsFilePaths.size() > idx) {
      String middlePath = className.substring(javaModelPackages.get(idx).length(),
          className.length() - cls.getSimpleName().length());
      path = new File(jsFilePaths.get(idx), middlePath.replace(".", File.separator));
    }
    return new File(path, cls.getSimpleName().toLowerCase() + ".js");
  }

  private String getJSClassName(boolean isReferClass, Class<?> cls) {
    if (isReferClass && jsReferClassMap.containsKey(cls.getName())) {
      return jsReferClassMap.get(cls.getName());
    }
    int pkgIdx = getJavaPackageIndex(cls.getName());
    if (pkgIdx < 0) {
      // this can not happen, because every class has passed the verification of
      // shouldBuildClass
      return cls.getName();
    }

    // get the prefix part of result js class name
    String prefix = javaModelPackages.get(pkgIdx);
    if (isReferClass && null != jsReferNamespaces && jsReferNamespaces.size() > pkgIdx) {
      prefix = jsReferNamespaces.get(pkgIdx);
    } else if (null != jsNamespaces && jsNamespaces.size() > pkgIdx) {
      prefix = jsNamespaces.get(pkgIdx);
    }

    // get the middle part index of result js class name, plus 1 here to exclude
    // the dot
    int middleStartIdx = javaModelPackages.get(pkgIdx).length() + 1;
    int middleEndIdx = cls.getName().lastIndexOf(".");

    // get the suffix part of result js class name
    List<String> suffixList = new ArrayList<String>();
    for (Class<?> c = cls; null != c; c = c.getDeclaringClass()) {
      suffixList.add(0, c.getSimpleName());
    }
    String suffix = StringUtils.join(suffixList, ".");

    if (middleStartIdx > middleEndIdx) {
      return StringUtils.join(new String[] { prefix, suffix }, ".");
    } else {
      String middle = cls.getName().substring(middleStartIdx, middleEndIdx);
      return StringUtils.join(new String[] { prefix, middle, suffix }, ".");
    }
  }

  private int getJavaPackageIndex(String className) {
    for (int i = 0; i < javaModelPackages.size(); ++i) {
      String packageName = javaModelPackages.get(i);
      if (className.startsWith(packageName + ".")) {
        return i;
      }
    }
    return -1;
  }

  private boolean shouldBuildClass(String className) {
    for (String packageName : javaModelPackages) {
      if ((null == javaModelClassPattern || javaModelClassPattern.matcher(className).matches())
          && className.startsWith(packageName + ".")) {
        return true;
      }
    }
    return false;
  }

  private void registerClass(Class<?> cls) {
    if (builtClassNames.contains(cls.getName())) {
      return;
    }
    buildingClasses.add(cls);
  }

  private void initDirectory(File path) throws Exception {
    if (!path.isDirectory()) {
      path.delete();
    }
    if (!path.exists()) {
      Files.createDirectories(path.toPath());
    }
  }

  private boolean initJsReferMap(BufferedReader br) throws Exception {
    jsReferClassMap.clear();
    String lineTxt = br.readLine();
    while (lineTxt != null) {
      lineTxt = lineTxt.trim();
      if (lineTxt.length() == 0 || lineTxt.startsWith("#")) {
        lineTxt = br.readLine();
        continue;
      }
      String[] kvEntry = lineTxt.split(":", 2);
      if (kvEntry.length != 2) {
        System.out.println(lineTxt + " should have format key:value");
        return false;
      }
      jsReferClassMap.put(kvEntry[0].trim(), kvEntry[1].trim());
      lineTxt = br.readLine();
    }
    return true;
  }

  public static void main(String[] args) {
    JSModelBuilder builder = new JSModelBuilder();
    CmdLineParser cmdParser = new CmdLineParser(builder);
    try {
      // parse the arguments.
      cmdParser.parseArgument(args);
    } catch (CmdLineException e) {
      System.err.println(e.getMessage());
      // print the list of available options
      cmdParser.printUsage(System.err);

      System.exit(1);
    }

    try {
      boolean buildRet = builder.build();
      if (!buildRet) {
        System.exit(1);
      }
    } catch (Exception e) {
      e.printStackTrace(System.err);
      ;
    }
  }
}
